#!/usr/bin/env python
#
# Simulated storage-related components
#
# Author:   Mike Murphy <mamurph@cs.clemson.edu>
# Revision: 23 September 2009
#
#   Copyright 2009 Clemson University
#
#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.
#
# This material is based upon work supported under a National Science
# Foundation Graduate Research Fellowship. Any opinions, findings,
# conclusions or recommendations expressed in this publication are those of
# the author(s) and do not necessarily reflect the views of the National
# Science Foundation.

from results import FileRequestResult, FileTransferResult


class DataFile(object):
   '''
   Simulation of a data file or other large piece of data that must be
   transferred over a Wide Area Network
   '''
   def __init__(self, name, size, **kwds):
      '''
      @param name: Name of the simulated file
      @type name: str
      @param size: Size of the simulated file in BYTES
      @type size: int
      @param kwds: File extra data
      '''
      self.name = name
      self.size = size
      self.extra_data = kwds
   #
   def transfer(self, sim, start_time, speed, callback, server_name, \
                client_name):
      '''
      Schedules a transfer of the DataFile from a server to a client. Transfer
      times are computed using the transfer speed. A transfer speed of zero is
      used to indicate that the transfer is instantaneous (for allowing
      experiments to enable/disable transfer time simulation)

      @param sim: Reference to the simulator instance
      @param start_time: Transfer start time
      @type start_time: number
      @param speed: Transfer speed in BYTES per second (see the L{units}
                    module for convenient constants)
      @type speed: number
      @param callback: Callback function to execute when the transfer
                       completes. This function must be simulator-aware
                       and will receive arguments: current time, simulator
                       reference, filename, a reference to this object,
                       the name of the server, the name of the client, and
                       the transfer speed
      @param server_name: Name of the server from which the file is to be
                          transferred
      @type server_name: str
      @param client_name: Name of the client to which the file is to be
                          transferred
      @type client_name: str
      '''
      if speed > 0:
         xfer_time = self.size / speed
         end_time = start_time + xfer_time
      else:
         # Transfer is instant if speed is non-positive. This "feature"
	 # allows the file server/client pair to be used on a local site,
	 # permitting one pilot job implementation to cover several cases.
         end_time = start_time
      if callback:
         sim.new_simple_event(end_time, callback, self.name, self, \
	                      server_name, client_name, speed)
   def __str__(self):
      return '%s (%d)' % (self.name, self.size)
#


class FileCache(object):
   '''
   Transparent file cache: transfers a file if necessary; otherwise, raises
   a callback event at the current simulator clock time to notify client code
   that the simulated file is ready for simulated use. Note that this cache
   does not provide any internal expiration mechanism. Once a file has been
   received into the cache, it remains in the cache permanently.
   '''
   def __init__(self, name, sim, speed, **kwds):
      '''
      @param name: Name of the file cache
      @type name: str
      @param sim: Reference to the simulator instance
      @param speed: Speed of the network link into the file cache, in
                    BYTES per second (see L{units} for constants). A speed of
                    zero will result in instantaneous transfers.
      @type speed: number
      @param kwds: Extra data
      '''
      self.name = name
      self.sim = sim
      self.speed = speed
      self.extra_data = kwds
      self.cache = {}
      self.pending = {}
      self.stores = []
   def xfer_complete(self, cur_time, simobj, filename, datafile, srv_n, \
                    cli_n, speed):
      '''
      Method invoked by a simulator event when a file becomes available in
      the file cache. Notifies client code of file availability by means of
      a callback function.

      @param cur_time: Current simulated time
      @type cur_time: number
      @param simobj: Reference to the simulator object
      @param filename: Name of the file
      @type filename: str
      @param datafile: Reference to the DataFile object
      @param srv_n: Name of the file server
      @type srv_n: str
      @param cli_n: Name of the file cache
      @type cli_n: str
      @param speed: Transfer speed in BYTES per second (see L{units})
      @type speed: number

      @rtype: L{results.FileTransferResult}
      '''
      self.cache[filename] = datafile
      for callback in self.pending[filename]:
         self.sim.new_simple_event(cur_time, callback, filename, datafile)
      del self.pending[filename]
      size = datafile.size
      ts = simobj.get_timestamp()
      return FileTransferResult(ts, filename, size, srv_n, cli_n, speed)
   def get_file(self, filename, callback):
      '''
      Requests a file from the cache. If the file is not in the cache, all
      known servers are queried to locate the file, and the file is
      transferred into the cache. If the file cannot be found on any server,
      an Exception occurs.

      This method works asynchronously: client code must specify a callback
      function, which will take two arguments: the filename and the DataFile
      instance itself. This callback will be invoked once the file becomes
      available in the cache.

      @param filename: Name of the DataFile to request
      @type filename: str
      @param callback: Callback function to invoke when the file becomes
                       available

      @rtype: L{results.FileRequestResult}
      '''
      now = self.sim.get_time()
      nowstamp = self.sim.get_timestamp()
      cached = False
      if filename in self.cache:
         datafile = self.cache[filename]
         self.sim.new_simple_event(now, callback, filename, datafile)
	 cached = True
      else:
         if filename in self.pending:
	    self.pending[filename].append(callback)
	 else:
	    found = False
	    for store in self.stores:
	       if store.has_file(filename):
	          store.get_file(filename, self.name, self.speed, \
		                 self.xfer_complete)
		  found = True
		  break
	    #
	    if found:
	       self.pending[filename] = [ callback ]
            else:
	       raise Exception('Simulated file not found in any store')
      return FileRequestResult(nowstamp, filename, self.name, cached)
   def add_file(self, datafile):
      '''
      Adds a file directly into the cache, without transferring it across
      the network

      @param datafile: DataFile instance to add to the cache
      '''
      filename = datafile.name
      self.cache[filename] = datafile
   def flush_file(self, filename):
      '''
      Removes a file from the cache

      @param filename: Name of the file to remove
      @type filename: str
      '''
      del self.cache[filename]
   def add_store(self, store):
      '''
      Adds a FileStore (server component) that can be queried to find files
      by name and transfer them into the cache

      @param store: FileStore instance to add
      '''
      if store not in self.stores:
         self.stores.append(store)
   def remove_store(self, store):
      '''
      Removes a FileStore from the list of servers that can be queried

      @param store: FileStore instance to remove
      '''
      self.stores.remove(store)
   def __str__(self):
      result = 'FileCache: %s\n' % self.name
      for df in self.cache:
         result += '   ' + str(df) + '\n'
      return result
#


class FileStore(object):
   '''
   Simulated file server, which stores DataFile instances using a filename
   map. Filenames must be unique within each FileStore.
   '''
   def __init__(self, name, sim, speed, **kwds):
      '''
      @param name: Name of this file server
      @type name: str
      @param sim: Reference to the simulator object
      @param speed: Network link speed in BYTES per second (see L{units})
      @type speed: number
      @param kwds: Extra data
      '''
      self.name = name
      self.speed = speed
      self.sim = sim
      self.extra_data = kwds
      self.filemap = {}
   def has_file(self, filename):
      '''
      @param filename: Name of the file to check
      @type filename: str

      @rtype: bool
      @return: True iff the requested filename is available from this server
      '''
      return filename in self.filemap
   def get_file(self, filename, cache_name, local_speed, callback):
      '''
      Requests a file transfer from the server to a client. This transfer is
      performed using the transfer() method of the DataFile object itself,
      using the minimum of the server network link speed and client network
      link speed. Transfers are asynchronous: a call to this method will
      schedule a simulator event at the same time, or in the future, that
      will execute the callback function with the transfer result. If the
      file cannot be found in the data store, an Exception will occur.

      @param filename: Name of the file to transfer
      @type filename: str
      @param cache_name: Name of the client making the request
      @type cache_name: str
      @param local_speed: Local network link speed of the client in BYTES
                          per second (see L{units})
      @type local_speed: number
      @param callback: Transfer callback function. See the documentation
                       for L{DataFile.transfer} for details.
      '''
      if filename not in self.filemap:
         raise Exception('Simulated file not found in store: %s' % self.name)
      now = self.sim.get_time()
      datafile = self.filemap[filename]
      datafile.transfer(self.sim, now, min(self.speed, local_speed), \
         callback, self.name, cache_name)
   def add_file(self, datafile):
      '''
      Adds a file to the store

      @param datafile: DataFile instance to add to the server store
      '''
      filename = datafile.name
      self.filemap[filename] = datafile
   def remove_file(self, filename):
      '''
      Removes a file from the server store

      @param filename: Name of the file to remove
      @type filename: str
      '''
      del self.filemap[filename]
   def __str__(self):
      result = 'FileStore: %s\n' % self.name
      for name in self.filemap:
         result += '   ' + str(name) + '\n'
      return result
#
